PRELOADER

当前文章 : 《机器学习实战——学习笔记(一) k-近邻算法(手写识别系统实战)》

12/2/2019 —— 

机器学习实战——学习笔记(一) k-近邻算法(手写识别系统实战)

一、k-近邻法简介

k近邻法(k-nearest neighbor, k-NN)是1967年由Cover T和Hart P提出的一种基本分类与回归方法。它的工作原理是:存在一个样本数据集合,

也称作为训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一个数据与所属分类的对应关系。输入没有标签的新数据后,

将新的数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集

中前k个最相似的数据,这就是k-近邻算法中k的出处,通常k是不大于20的整数。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。

说白了,就是将新数据和训练集中每个数据进行比较,找到距离新数据最近的K个数据,对这K个数据的标签进行统计,支持数最高的标签即可认为

是新数据的标签。比如K=10,其中有9的标签是A类,1个的标签是B类,那么这个新数据的标签被认为是A。

二、手写识别系统问题

1:背景知识

对于需要识别的数字已经使用图形处理软件,处理成具有相同的色彩和大小:宽高是32像素x32像素。尽管采用本文格式存储图像不能有效地利用内存空间,但是为了方便理解,我们将图片转换为文本格式,数字的文本格式如图所示。

与此同时,这些文本格式存储的数字的文件命名也很有特点,格式为:数字的值_该数字的样本序号

对于这样已经整理好的文本,我们可以直接使用Python处理,进行数字预测。数据集分为训练集和测试集,使用K-近邻的方法,

自己设计k-近邻算法分类器,可以实现分类。数据集和实现代码下载地址:https://github.com/miraitowa/Machine-Learning/tree/master/KNN/3.%E6%95%B0%E5%AD%97%E8%AF%86%E5%88%AB

2、sklearn简介

Scikit learn 也简称sklearn,是机器学习领域当中最知名的python模块之一。sklearn包含了很多机器学习的方式:

1. Classification 分类
1. Regression 回归
1. Clustering 非监督分类
1. Dimensionality reduction 数据降维
1. Model Selection 模型选择
1. Preprocessing 数据与处理

使用sklearn可以很方便地让我们实现一个机器学习算法。一个复杂度算法的实现,使用sklearn可能只需要调用几行API即可。所以学习sklearn,可以有效减少我们特定任务的实现周期。

3、sklearn安装

由于我使用Anaconda安装,推荐Anaconda,因为里面已经内置了NumPy,SciPy等常用工具

conda install scikit-learn

具体安装教程以及步骤可以查阅:http://blackblog.tech/2018/02/05/%E5%8D%81%E5%88%86%E9%92%9F%E4%B8%8A%E6%89%8Bsklearn-1/

sklearn小试牛刀

我们知道数字图片是32x32的二进制图像,为了方便计算,我们可以将32x32的二进制图像转换为1x1024的向量。对于sklearn的KNeighborsClassifier输入可以是矩阵,不用一定转换为向量,不过为了跟自己写的k-近邻算法分类器对应上,这里也做了向量化处理。

然后构建kNN分类器,利用分类器做预测

# -*- coding: UTF-8 -*-
import numpy as np
import operator
from os import listdir
from sklearn.neighbors import KNeighborsClassifier as kNN



"""
函数说明:将32x32的二进制图像转换为1x1024向量。

Parameters:
    filename - 文件名
Returns:
    returnVect - 返回的二进制图像的1x1024向量

"""
def img2vector(filename):
    #创建1x1024零向量
    returnVect = np.zeros((1, 1024))
    #打开文件
    fr = open(filename)
    #按行读取
    for i in range(32):
        #读一行数据
        lineStr = fr.readline()
        #每一行的前32个元素依次添加到returnVect中
        for j in range(32):
            returnVect[0, 32*i+j] = int(lineStr[j])
    #返回转换后的1x1024向量
    return returnVect

"""
函数说明:手写数字分类测试

Parameters:
    无
Returns:
    无

"""
def handwritingClassTest():
    #测试集的Labels
    hwLabels = []
    #返回trainingDigits目录下的文件名
    trainingFileList = listdir('trainingDigits')
    #返回文件夹下文件的个数
    m = len(trainingFileList)
    #初始化训练的Mat矩阵,测试集
    trainingMat = np.zeros((m, 1024))
    #从文件名中解析出训练集的类别
    for i in range(m):
        #获得文件的名字
        fileNameStr = trainingFileList[i]
        #获得分类的数字
        classNumber = int(fileNameStr.split('_')[0])
        #将获得的类别添加到hwLabels中
        hwLabels.append(classNumber)
        #将每一个文件的1x1024数据存储到trainingMat矩阵中
        trainingMat[i,:] = img2vector('trainingDigits/%s' % (fileNameStr))
    #构建kNN分类器
    neigh = kNN(n_neighbors = 3, algorithm = 'auto')
    #拟合模型, trainingMat为测试矩阵,hwLabels为对应的标签
    neigh.fit(trainingMat, hwLabels)
    #返回testDigits目录下的文件列表
    testFileList = listdir('testDigits')
    #错误检测计数
    errorCount = 0.0
    #测试数据的数量
    mTest = len(testFileList)
    #从文件中解析出测试集的类别并进行分类测试
    for i in range(mTest):
        #获得文件的名字
        fileNameStr = testFileList[i]
        #获得分类的数字
        classNumber = int(fileNameStr.split('_')[0])
        #获得测试集的1x1024向量,用于训练
        vectorUnderTest = img2vector('testDigits/%s' % (fileNameStr))
        #获得预测结果
        # classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
        classifierResult = neigh.predict(vectorUnderTest)
        print("分类返回结果为%d\t真实结果为%d" % (classifierResult, classNumber))
        if(classifierResult != classNumber):
            errorCount += 1.0
    print("总共错了%d个数据\n错误率为%f%%" % (errorCount, errorCount/mTest * 100))


"""
函数说明:main函数

Parameters:
    无
Returns:
    无
"""
if __name__ == '__main__':
    handwritingClassTest()

上述代码使用的algorithm参数是auto,更改algorithm参数为brute,使用暴力搜索,你会发现,运行时间变长了,变为10s+。

更改n_neighbors参数,你会发现,不同的值,检测精度也是不同的。自己可以尝试更改这些参数的设置,加深对其函数的理解。

而进行简单的优化处理就会发现

# -*- coding:utf-8 -*-
from numpy import *
import operator
from os import listdir
import matplotlib.pyplot as plt
from sklearn.neighbors import KNeighborsClassifier as kNN

"""函数说明:将32x32的二进制图像转换为1x1024向量。"""

def img2vector(filename): 
    #创建1x1024零向量
    returnVect = np.zeros((1,1024))
    #打开文件
    fr = open(filename)
    #按行读取
    for i in range(32):
         #读一行数据
        lineStr = fr.readline()
        #每一行的前32个元素依次添加到returnVect中
        for j in range(32):
            returnVect[0,32*i+j] = int(lineStr[j])
            #返回转换后的1x1024向量
    return returnVect

"""函数说明:手写数字分类测试"""

def handwritingClassTest():
     #测试集的Labels
    hwLabels = []
    #返回trainingDigits目录下的文件名
    trainingFileList = listdir('trainingDigits')
    #返回文件夹下文件的个数
    m = len(trainingFileList)
    #初始化训练的Mat矩阵,测试集
    trainingMat = np.zeros((m,1024))
    #从文件名中解析出训练集的类别
    for i in range(m):
         #获得文件的名字
        fileNameStr = trainingFileList[i]
         #获得分类的数字
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
         #将获得的类别添加到hwLabels中
        hwLabels.append(classNumStr)
          #将每一个文件的1x1024数据存储到trainingMat矩阵中
        trainingMat[i,:] = img2vector('trainingDigits/%s' % fileNameStr)
    testFileList = listdir('testDigits')
    errorCount = 0.0
    mTest = len(testFileList)
    for i in range(mTest):
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        vectorUnderTest = img2vector('testDigits/%s' % fileNameStr)
        classifierResult = classify(vectorUnderTest,trainingMat,hwLabels,3)
        print ("the classifier came back with: %d, the real answer is: %d"  % (classifierResult,classNumStr) )
        if (classifierResult != classNumStr): errorCount += 1.0
    print("\nthe total number of errors is: %d" % errorCount)
    print("\nthe total error rate is: %f" % (errorCount / float(mTest)))

if __name__ == '__main__':
    handwritingClassTest()

四、总结

1、kNN算法的优缺点

优点

简单好用,容易理解,精度高,理论成熟,既可以用来做分类也可以用来做回归;

可用于数值型数据和离散型数据;

训练时间复杂度为O(n);无数据输入假定;

对异常值不敏感

缺点

计算复杂性高;空间复杂性高;

样本不平衡问题(即有些类别的样本数量很多,而其它样本的数量很少);

一般数值很大的时候不用这个,计算量太大。但是单个样本又不能太少,否则容易发生误分。

最大的缺点是无法给出数据的内在含义。